package application.models.collections;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import application.models.Observer;
import application.models.Observable;
import application.models.collections.Collection;
import application.models.models.CustomerModel;
import application.models.exceptions.InvalidAttributeException;
import application.models.datalayer.Attribute;
import application.models.datalayer.DataCondition;
import application.models.datalayer.DataLayerCommunicator;

/**
 * This class handles collections of customer models
 * 
 * @author Philip Rasmussen & Morten Therkildsen
 */
public class CustomerCollection implements Collection<CustomerModel> {

	private static CustomerModel modelInstance = new CustomerModel();

	private Set<DataCondition> filters;
	private Set<DataCondition> appliedFilters;
	private Set<CustomerModel> models;
	private Set<Observer> observers;
	private Observer observer;
	private boolean isReloading;

	/**
	 * Class constructor
	 */
	public CustomerCollection(){

		filters = new HashSet<DataCondition>();
		appliedFilters = new HashSet<DataCondition>();
		models = new HashSet<CustomerModel>();
		observers = new HashSet<Observer>();
		isReloading = false;
		
		observer = new Observer() {
			
			public void notify(Observable subject){
				
				if(isReloading) return;
				
				// If the subject is a CustomerModel, we might need to add/remove it to/from our collection!
				if(subject instanceof CustomerModel){
					
					CustomerModel model = (CustomerModel) subject;
					
					if(model.isDeleted()) models.remove(model);
					else try{

						loadHelper(appliedFilters.toArray(new DataCondition[0])); // Load!
					} catch(IOException e){/* IGNORE */}
				}
			}
		};
		
		CustomerModel.addGlobalObserver(observer);
	}

	/**
	 * Loads all models of its kind filtered by previously set filters
	 * If no filters are set, all models will be loaded
	 * 
	 * @throws IOException	if the collection failed to load its data
	 */
	public void load() throws IOException {
	
		appliedFilters = new HashSet<DataCondition>(filters); // Use the currently set filters
		loadHelper(appliedFilters.toArray(new DataCondition[0])); // Load!
	}
	
	/**
	 * A helper class doing the actual loading for load()
	 * 
	 * @param allConditions		the conditions to filter which models to load
	 */
	private void loadHelper(DataCondition[] allConditions) throws IOException {

		isReloading = true;
		
		// Remove all previously set elements (if any) - we are reloading!
		// NB: The collection must be replaced to prevent ConcurrentModificationExceptions from iterators
		// Simply removing the content won't do
		models = new HashSet<CustomerModel>();

		// STEP 1: Sort the conditions into two collections, one for each data set (main set + set to be joined with)

		// Construct a HashMap of attributes from the main set
		try{

			Attribute[] mainSetAttributes = DataLayerCommunicator.getInstance().getDataSetDescription(CustomerModel.getMainSetName());
			Map<String, Attribute> mainSetAttributeMap = new HashMap<String, Attribute>();
			for(Attribute mainSetAttribute : mainSetAttributes) mainSetAttributeMap.put(mainSetAttribute.getName(), mainSetAttribute);

			// Sort the defined conditions into two ArrayList, one for each set
			ArrayList<DataCondition> mainSetConditions = new ArrayList<DataCondition>();
			ArrayList<DataCondition> joinSetConditions = new ArrayList<DataCondition>();

			for(DataCondition condition : allConditions){

				if(mainSetAttributeMap.containsKey(condition.getAttributeName())) mainSetConditions.add(condition);
				// If the attribute in the condition is not from the main set, it is from the join set
				// This is guaranteed by the addFilter method
				else joinSetConditions.add(condition);
			}

			// STEP 2:	Find the entities from each set matching their respective conditions. Only get id + join key for both sets

			Attribute[][] mainSetEntities = DataLayerCommunicator.getInstance().readFromDataLayer(CustomerModel.getMainSetName(), mainSetConditions.toArray(new DataCondition[0]), new String[]{CustomerModel.getMainSetIdAttribute(), CustomerModel.getMainSetJoinAttribute()});
			Attribute[][] joinSetEntities = null;

			if(CustomerModel.getJoinedSetName() != null){

				joinSetEntities = DataLayerCommunicator.getInstance().readFromDataLayer(CustomerModel.getJoinedSetName(), joinSetConditions.toArray(new DataCondition[0]), new String[]{CustomerModel.getJoinedSetJoinAttribute()});	
			}


			// STEP 3: Determine what models to load if we got set matches

			if(mainSetEntities.length > 0 && (CustomerModel.getJoinedSetName() == null || joinSetEntities.length > 0)){

				int mainSetAttributeIdIndex = mainSetEntities[0][0].getName().equals(CustomerModel.getMainSetIdAttribute()) ? 0 : 1;
				
				ArrayList<Integer> ids = new ArrayList<Integer>();

				if(joinSetEntities == null) for(Attribute[] entity : mainSetEntities) ids.add((Integer) entity[mainSetAttributeIdIndex].getValue());
				else {

					int mainSetJoinAttributeIndex = mainSetEntities[0][1].getName().equals(CustomerModel.getMainSetJoinAttribute()) ? 1 : 0;
					
					Map<Object, Integer> mainIdsByMainJoinAttribute = new HashMap<Object, Integer>();
					for(Attribute[] entity : mainSetEntities) mainIdsByMainJoinAttribute.put(
																								entity[mainSetJoinAttributeIndex].getValue(),
																								(Integer) entity[mainSetAttributeIdIndex].getValue()
																							);
					for(Attribute[] entity : joinSetEntities){

						if(mainIdsByMainJoinAttribute.containsKey(entity[0].getValue())) ids.add(mainIdsByMainJoinAttribute.get(entity[0].getValue()));
					}
				}

				// STEP 4: Load the models that in the end matched the conditions

				for(int id : ids) models.add(CustomerModel.load(id));
			}

			// STEP 5: Notify observers
			notifyObservers();

		} catch(IOException e){
			throw new IOException("Failed to load collection.", e);
		} finally {
			
			isReloading = false;
		}
	}

	/**
	 * Adds a filter to the collection.
	 * 
	 * @param condition		A condition for one of the attributes of the entity
	 * 
	 * @throws InvalidAttributeException if the model does not support the attribute used by the condition
	 */
	public void addFilter(DataCondition filter) throws InvalidAttributeException {

		if(!(
				filter.getAttributeName().equals(CustomerModel.getMainSetIdAttribute()) ||
				filter.getAttributeName().equals(CustomerModel.getMainSetJoinAttribute())
		)) modelInstance.getAttribute(filter.getAttributeName()); // Will trigger a InvalidAttributeException if the condition attribute is not supported

		filters.add(filter);
	}

	/**
	 * Removes a filter from the collection.
	 * If the filter is not present, the method does nothing
	 * 
	 * @param condition		A condition for one of the attributes of the entity that is to be removed
	 */
	public void removeFilter(DataCondition filter){

		filters.remove(filter);
	}

	/**
	 * Returns all currently set filters
	 * 
	 * @return	An array of the set filters
	 */
	public DataCondition[] getFilters(){

		return filters.toArray(new DataCondition[0]); // Determines return type based on parameter
	}
	
	/**
	 * Returns all filters that was set prior to the latest call to load load()
	 * 
	 * @return	An array of the applied filters
	 */
	public DataCondition[] getAppliedFilters(){
		
		return appliedFilters.toArray(new DataCondition[0]); // Determines return type based on parameter
	}

	/**
	 * Used for iteration
	 * 
	 * @return an iterator which can used to iterate over the collection
	 */
	public Iterator<CustomerModel> iterator(){

		return models.iterator();
	}
	
	/**
	 * How many models the collection contains
	 * 
	 * @return the number of models in the collection
	 */
	public int size(){
		
		return models.size();
	}

	/**
	 * Registers an observer for the object
	 * An observer may only be added once - subsequent adds of the same observer are ignored
	 * 
	 * @param observer		The observer to add
	 */
	public void addObserver(Observer observer){

		if(observer == null) throw new NullPointerException("observer was null!");
		observers.add(observer);
	}

	/**
	 * Removes a previously set observer from the observable object
	 * Nothing will happen if observer has not been added beforehand
	 * 
	 * @param observer		A previously added observer
	 */
	public void removeObserver(Observer observer){

		observers.remove(observer);
	}

	/**
	 * Notifies all observers that are set for the collection
	 */
	private void notifyObservers(){

		for(Observer observer : observers) observer.notify(this);
	}
}